#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266httpUpdate.h>
#include <WiFiClientSecure.h>

#include "main.h"
#include "taskpool.h"
#include "assitant.h"
#include "mqtts.h"

#include "LittleFS.h"
#define fs LittleFS
#define max_local_items_number 1000

const char *config_file = "/config.txt";
const char *config_file_back = "/config_back.txt";
const char *file_sensor_data_local = "/se.txt";
const char *file_sensor_data_info = "/sef.txt";

// int cmd_read_config(int n, char *args[]);
int cmd_write_config(int n, char *args[]);
int cmd_regist_dev(int n, char *args[]);
int cmd_load_config(int, char *args[]);
int cmd_sys_n(int n, char *args[]);

static int sys_n = 0;
int cmd_ota_update(int n, char *args[]);
void config_setup()
{
    // new_cmd("read", cmd_read_config);
    public_a_var_ref("__fw__", (void *)(&__firmware_version), 'd');

    new_cmd("sys", cmd_sys_n);
    new_cmd("http", cmd_regist_dev);
    load_config();
    new_cmd("file", cmd_write_config);
    new_cmd("load", cmd_load_config);
    new_cmd("ota", cmd_ota_update);
}
const int httpsPort = 443;
const char fingerprint[] PROGMEM = "A071B3E053DF28BB25FDE8756AEA08A5097204E2";
const char *host = "glwang.site";

int cmd_regist_dev(int n, char *args[])
{
    WiFiClientSecure httpsClient;
    httpsClient.setFingerprint(fingerprint);
    httpsClient.setTimeout(15000);
    var _ok = stop_schedule();
    int r = 0;
    while ((!httpsClient.connect(host, httpsPort)) && (r < 30))
    {
        delay(100);
        Serial.print(".");
        r++;
    }
    Serial.print("\n");
    if (r == 30)
    {
        Serial.println("Connection failed");
        if (_ok)
            resume_schedule();
        return -1;
    }
    else
    {
        Serial.println("Connected to server, start registing, this may takes a bit long time.");
    }

    sprintf(tx, "GET /enroll/?id=%s", args[1]);
    String url(tx);

    String request = String(tx) + " HTTP/1.1\r\n" +
                     "Host: " + host + "\r\n" +
                     "Connection: close\r\n" +
                     "\r\n";
    httpsClient.print(request);

    int i = 0;
    int code = -1;
    while (httpsClient.connected())
    {
        i++;
        String line = httpsClient.readStringUntil('\n');
        if (i == 1)
        {
            var from = line.indexOf(' ');
            var s2 = line.substring(from + 1);
            var to = s2.indexOf(' ');
            s2 = s2.substring(0, to);
            code = s2.toInt();
            if (code != 200)
            {
                httpsClient.stop();
                if (_ok)
                    resume_schedule();
                return -code;
            }
        }
        // HTTP/1.1 200 OK
        if (line == "\r")
        {
            break;
        }
    }
    Serial.println("==========");
    // {"ok": 0, "msg": "ok", "username": "thingidp@aahqtdc|dev22", "password": "fb9b2cca8634e7ebf21a631a64a2aef9", "time": "2022/3/20 18:17:20", "dev": "dev22"}
    String s;
    while (httpsClient.available())
    {
        s += httpsClient.readStringUntil('\n'); // Read Line by Line
    }
    Serial.println(s);
    Serial.println("==========");
    httpsClient.stop();
    if (n < 3)
    {
        if (_ok)
            resume_schedule();
        return 1;
    }
    String ss = parse_string(s, "ok");
    code = ss.toInt();
    if (code != 0)
    {
        if (_ok)
            resume_schedule();
        return code;
    }
    const char *p[] = {"dev", "username", "password", "time"};
    for (int i = 0; i < 4; i++)
    {
        sprintf(tx, "file -d $$%s=", p[i]); // delete
        do_cmd(tx);
        ss = parse_string(s, p[i]);
        ss = ss.substring(1, ss.length() - 1);
        sprintf(tx, "$%s=%s", p[i], ss.c_str()); // update
        do_cmd(tx);
        // save
        file_prepend(tx);
    }
    // if (sensor_n == 0)
    // {
    //     strcpy(tx, "sensor dht11:D2");
    //     file_append(tx);
    //     do_cmd(tx);
    // }
    if (!had_timer(timer_sleep))
    {
        strcpy(tx, "slp 60");
        file_append(tx);
        do_cmd(tx);
    }
    if (_ok)
        resume_schedule();
    return 0;
}

String parse_string(String &s, String &key)
{
    key = "\"" + key + "\"" + ":";
    int ind = s.indexOf(key);
    // {"ok": 0, "time": "2022/1/26 10:2:37"}
    String res;
    if (ind < 0)
        return res;

    s.setCharAt(s.length() - 1, ',');
    ind += key.length();
    int ind2 = s.indexOf(',', ind);
    if (ind2 < 0)
        return res;
    res = s.substring(ind, ind2);
    res.trim();
    return res;
}
String parse_string(String &s, const char *key)
{
    String k = key;
    return parse_string(s, k);
}

// stop_schedule here
int register_this_dev()
{
    int id = ESP.getChipId();
    sprintf(tx_back, "%x", id);
    xout_("chip ID:");
    xouts(tx_back);
    char *args[3];
    args[1] = tx_back;
    var code = cmd_regist_dev(3, args);
    if (code < 0)
    {
        xout_("fail to regist this device, return code=");
        xouts(code);
    }
    return 0;
}

int mqtt_loop();
int read_local_data_info();
int read_local_data();

int cmd_write_config(int n, char *args[])
{
    if (n < 2)
    {
        // bool need_resume = false;
        stop_schedule();
        File f = fs.open(config_file, "r");
        if (!f)
        {
            resume_schedule();
            return -__LINE__;
        }
        int now = 0;
        while (f.available())
        {
            String s = f.readStringUntil('\n');
            var i = s.indexOf('\r');
            if (i >= 0)
            {
                s.replace("\r", "");
            }
            now++;
            out_(now);
            out_(':');
            mqtt_loop();
            outs(s);
            mqtt_loop();
        }
        f.close();
        resume_schedule();
        return 0;
    }
    String mode(args[1]);
    int i = 1;
    const char *f_mode = "a";
    if (mode == "-w")
    {
        f_mode = "w";
        i++;
    }
    else if (mode == "-d")
    {
        if (n < 2)
            return -__LINE__;
        var o = is_int(args[2]);
        int line;
        if (o)
        {
            line = Arg(2).toInt();
            if (line <= 0)
            {
                return -__LINE__;
            }
        }
        int now = 0;
        File a = fs.open(config_file, "r");
        if (!a)
        {
            return -__LINE__;
        }
        File b = fs.open(config_file_back, "w");
        if (!b)
        {
            a.close();
            return -__LINE__;
        }
        while (a.available())
        {
            now++;
            String s = a.readStringUntil('\n');
            if (o)
            {
                if (now == line)
                    continue;
            }
            else
            {
                if (s.startsWith(args[2]))
                    continue;
            }
            b.println(s);
        }
        a.close();
        b.close();
        a = fs.open(config_file, "w");
        if (!a)
        {
            return -__LINE__;
        }
        b = fs.open(config_file_back, "r");
        if (!b)
        {
            a.close();
            return -__LINE__;
        }

        while (b.available())
        {
            String s = b.readStringUntil('\n');
            a.println(s);
        }
        a.close();
        b.close();
        return 0;
    }
    else if (mode == "-p")
    {
        if (n < 3)
            return -__LINE__;
        String s(args[2]);
        for (int i = 3; i < n; i++)
        {
            s.concat(' ');
            var ok = s.concat(args[i]);
            if (!ok)
            {
                return -__LINE__;
            }
        }
        file_prepend(s.c_str());
        outs(s);
        return 0;
    }
    else if (mode == "-h")
    {
        outs("file text :append text");
        mqtt_loop();
        outs(" -w text :overwrite");
        mqtt_loop();
        outs(" -d line|head :delete a line or item start with head");
        mqtt_loop();
        outs(" -p text : prepend text to file");
        mqtt_loop();
        outs(" -h : help doc");
        mqtt_loop();
        outs(" -i : local data info");
        mqtt_loop();
        outs(" -l : read local data");
        return 0;
    }
    else if (mode == "-i")
    {
        return read_local_data_info();
    }
    else if (mode == "-l")
    {
        return read_local_data();
    }

    File f = fs.open(config_file, f_mode);
    if (!f)
        return -__LINE__;
    for (; i < n - 1; i++)
    {
        f.write((const char *)args[i]);
        f.write(" ");
    }
    f.write((const char *)(args[i]));
    f.write("\n");
    f.close();
    return 0;
}
int file_append(const char *s, String end)
{
    var f = fs.open(config_file, "a");
    if (!f)
        return -__LINE__;
    f.write(s);
    if (!end.isEmpty())
        f.print(end);
    f.close();
    return 0;
}
int file_prepend(const char *s, String end)
{
    int now = 0;
    File a = fs.open(config_file, "r");
    if (!a)
    {
        return -__LINE__;
    }
    File b = fs.open(config_file_back, "w");
    if (!b)
    {
        a.close();
        return -__LINE__;
    }
    while (a.available())
    {
        now++;
        String s = a.readStringUntil('\n');
        b.println(s);
    }
    a.close();
    b.close();
    a = fs.open(config_file, "w");
    if (!a)
    {
        return -__LINE__;
    }
    b = fs.open(config_file_back, "r");
    if (!b)
    {
        a.close();
        return -__LINE__;
    }
    a.write(s);
    if (!end.isEmpty())
        a.print(end);
    while (b.available())
    {
        String s = b.readStringUntil('\n');
        a.println(s);
    }
    a.close();
    b.close();
    return 0;
}

int load_config()
{
    File f = fs.open(config_file, "r");
    if (!f)
    {
        return -__LINE__;
    }
    while (f.available())
    {
        String s = f.readStringUntil('\n');
        var i = s.indexOf('\r');
        if (i >= 0)
        {
            s.replace("\r", "");
        }
        i = s.indexOf('\n');
        if (i >= 0)
        {
            s.replace("\n", "");
        }
        xout_(">>");
        xouts(s);
        strcpy(tx, s.c_str());
        do_cmd(tx);
        stop_schedule(); // if in case ticker is started in a cmd
    }
    f.close();
    // if (sys_n == 0)
    // {
    //     file_append("sys 0");
    // }
    return 0;
}
int cmd_load_config(int, char *args[])
{
    return load_config();
}

int update_sys_n()
{
    out_("update sys to ");
    outs(sys_n);
    strcpy(tx, "file -d sys");
    var ok = do_cmd(tx);
    if (ok < 0)
    {
        return 1;
    }
    sprintf(tx, "sys %d", sys_n);
    ok = file_append(tx);
    if (ok < 0)
        return 1;
    return 0;
}
int cmd_sys_n(int n, char *args[])
{
    if (n < 2)
    {
        sprintf(tx, "version: %s %s", __DATE__, __TIME__);
        outs(tx);
        out_("start time:");
        outs(sys_n);
        return 0;
    }
    var ok = is_int(args[1]);
    if (!ok)
    {
        out_("invalid:\"");
        out_(args[1]);
        outs('\"');
        return -__LINE__;
    }
    n = Arg(1).toInt();
    sys_n = n + 1;
    addTask_(update_sys_n);
    return 0;
}

String cmd_search_file(const char *item)
{
    File f = fs.open(config_file, "r");
    if (!f)
    {
        return "";
    }
    while (f.available())
    {
        String s = f.readStringUntil('\n');
        var i = s.indexOf('\r');
        if (i >= 0)
        {
            s.replace("\r", "");
        }
        if (s.startsWith(item))
        {
            f.close();
            return s;
        }
    }
    f.close();
    return "";
}

String cmd_search_file(_File_search_callback_handle callback_handle, int &out_line)
{
    File f = fs.open(config_file, "r");
    if (!f)
    {
        return "";
    }
    int line = 0;
    while (f.available())
    {
        String s = f.readStringUntil('\n');
        var i = s.indexOf('\r');
        if (i >= 0)
        {
            s.replace("\r", "");
        }
        line++;
        var ok = callback_handle(s);
        if (ok)
        {
            out_line = line;
            f.close();
            return s;
        }
    }
    f.close();
    return "";
}

// @ using tx
// @ time cost
// make sure you have current time
// and start_time is valid
// or we cannot determine right timestamp
int clean_local_data(int count, time_t from_time)
{
    // if do not given a start time
    // we can only discrad these data
    if (!from_time)
        return -__LINE__;
    var now = service_time();
    if (!now)
        return -__LINE__;
    var delta_time = now - from_time;
    int time_step = delta_time / count;
    // report
    xouts("start clean local data");
    xout_("count:");
    xouts(count);
    xout_("from ");
    str_tm(tx, from_time);
    xouts(tx);
    xout_("to ");
    str_tm(tx, now);
    xouts(tx);
    // start to resend
    var f = fs.open(file_sensor_data_local, "r");
    if (!f)
        return -__LINE__;
    int i;
    String top = "control/upload/" + client_id;
    var top_cs = top.c_str();

    for (i = 0; i < count; i++)
    {
        var n = f.readBytesUntil('\n', tx_back, tx_len);
        tx_back[n] = '\0';
        if (!n)
            break;
        // send out
        time_t t = from_time + time_step * i;
        str_tm(tx, t);
        String send_time = tx;
        sprintf(tx, "{\"sent_time\":\"%s\",\"data\":%s}", send_time.c_str(), tx_back);
        var ok = mqtt_client.publish(top_cs, tx);
        // {"send_time":2022-2-27 22:27:27,"data":{"temperature":19.90,"humidity":51.00}}
        if (!ok)
        {
            xouts("mqtt pub failed");
        }
        xouts(tx);
        mqtt_client.loop();
    }
    f.close();
    if (i < count)
    {
        xout_("count to:");
        xouts(i);
        xouts("[WARNING]:local data is wrong, time may be incorrect");
    }
    // clean it
    f = fs.open(file_sensor_data_local, "w");
    if (!f)
        return -__LINE__;
    f.close();
    return 0;
}
// @ using tx
// @ time cost
// call this only when system got a exactly right time
// update infos in the file
// and clean local's data if has
int update_local_info(time_t time)
{
    var a_yes = fs.exists(file_sensor_data_info);
    if (!a_yes) // not exist
    {
        var fa = fs.open(file_sensor_data_info, "w");
        if (!fa)
        {
            return -__LINE__;
        }
        sprintf(tx, "%d,%lld\n", 0, time);
        fa.write((const char *)tx);
        fa.close();
        xouts(tx);
    }
    var f = fs.open(file_sensor_data_info, "r");
    if (!f)
    {
        return -__LINE__;
    }
    var n = f.readBytes(tx, tx_len);
    f.close();
    tx[n] = '\0';
    var sp = strchr(tx, ',');
    if (!sp)
    {
        return -__LINE__;
    }
    *sp = '\0';
    var count = atoi(tx);
    if (count) // item stacked, need to send out
    {
        sp++;
        if (tx[n - 2] == '\r' || tx[n - 2] == '\n')
        {
            tx[n - 2] = '\0';
        }
        else if (tx[n - 1] == '\n' || tx[n - 1] == '\r')
        {
            tx[n - 1] = '\0';
        }
        var start_time = atoll(sp);
        var rtc = clean_local_data(count, start_time);
        if (rtc < 0)
        {
            xout_("clean end, return ");
            xouts(rtc);
        }
    }
    // overwrite time, and count is 0
    var fa = fs.open(file_sensor_data_info, "w");
    if (!fa)
    {
        return -__LINE__;
    }
    sprintf(tx, "%d,%lld\n", 0, time);
    fa.write((const char *)tx);
    fa.close();
    return 0;
}

// @ time cost
// @ using tx
// call when there's no internet connection
// update count=count+1, keep time same
// but if there's no time in the info file
// we cannot resend these data after on connection
// so discrad these data if no time in info
// append sensor data to file
int sensor_data_dump(const char *data)
{
    var a_yes = fs.exists(file_sensor_data_info);
    // var b_yes = fs.exists(file_sensor_data_local);
    if (!a_yes)
    {
        return -__LINE__;
    }
    var f = fs.open(file_sensor_data_info, "r");
    var n = f.readBytes(tx, tx_len);
    f.close();
    tx[n] = '\0';
    var sp = tx;
    if (tx[n - 2] == '\r' || tx[n - 2] == '\n')
    {
        tx[n - 2] = '\0';
    }
    else if (tx[n - 1] == '\n' || tx[n - 1] == '\r')
    {
        tx[n - 1] = '\0';
    }
    sp = strchr(tx, ',');
    if (!sp)
    {
        // no time
        return -__LINE__;
    }
    *sp = '\0';
    sp++;
    // see the time if exist
    var start_time = atoll(sp);
    if (!start_time)
    {
        return -__LINE__;
    }
    strcpy(tx_back, sp); // time string
    var count = atoi(tx);
    count++;
    if (count > max_local_items_number)
    {
        xouts("max_local_items_number");
        return -__LINE__;
    }
    // update count, keep time same
    // or you can use strcat
    sprintf(tx, "%d,", count);
    // itoa(count,tx ,10);
    strcat(tx, tx_back);
    f = fs.open(file_sensor_data_info, "w");
    if (!f)
    {
        return -__LINE__;
    }
    f.write((const char *)tx);
    f.close();
    //  append the data to local
    var yes = fs.exists(file_sensor_data_local);
    const char *mode = "a";
    if (!yes)
    {
        mode = "w";
    }
    f = fs.open(file_sensor_data_local, mode);
    if (!f)
    {
        return -__LINE__;
    }
    f.write(data);
    f.write('\n');
    f.close();
    return 0;
}
int read_local_data_info()
{
    var a_yes = fs.exists(file_sensor_data_info);
    if (!a_yes) // not exist
    {
        outs("file not exist");
        return 0;
    }

    var f = fs.open(file_sensor_data_info, "r");
    if (!f)
    {
        return -__LINE__;
    }
    var n = f.readBytes(tx, tx_len);
    f.close();
    tx[n] = '\0';
    if (tx[n - 1] == '\r')
        tx[n - 1] = '\0';
    xout_("tx:");
    xouts(tx);
    xout_("size:");
    xouts((int)n);
    var sp = strchr(tx, ',');
    if (!sp)
    {
        return -__LINE__;
    }
    *sp = '\0';
    var count = atoi(tx);
    xout_("count:");
    xouts(count);
    // count:0
    sp++;
    if (tx[n - 2] == '\r' || tx[n - 2] == '\n')
    {
        tx[n - 2] = '\0';
    }
    else if (tx[n - 1] == '\n' || tx[n - 1] == '\r')
    {
        tx[n - 1] = '\0';
    }
    xout_("time_t:");
    xouts(sp);
    var start_time = atoll(sp);
    str_tm(tx, start_time);
    xout_("at:");
    xouts(tx);
    return 0;
}

int read_local_data()
{
    var f = fs.open(file_sensor_data_local, "r");
    if (!f)
        return -__LINE__;
    int i = 0;
    while (1)
    {
        var n = f.readBytesUntil('\n', tx, tx_len);
        if (!n)
            break;
        i++;
        tx[n] = '\0';
        xouts(tx);
    }
    f.close();
    xout_("line:");
    xouts(i);
    return 0;
}

int cmd_ota_update(int n, char *args[])
{
    extern WiFiClient espClient;
    ESPhttpUpdate.rebootOnUpdate(true);
    var default_url = "http://glwang.site:8001/res/firmware.bin";
    char tx[128];

    if (n > 1)
    {
        if (args[1][0] == '-')
        {
            lily_out("to update firmware remotely\nusage:ota [url]\n");
            sprintf(tx, "current version: %d\n", __firmware_version);
            lily_out(tx);
            return 0;
        }
        if (strncmp(args[1], "http://", 7) != 0 && strncmp(args[1], "https://", 8) != 0)
        {
            if (strlen(args[1]) > sizeof(tx) - 32)
            {
                li_error("name too long", -__LINE__);
            }
            sprintf(tx, "http://glwang.site:8001/res/%s.bin", args[1]);
            default_url = tx;
        }
        else
            default_url = args[1];
    }
    String fetch_url(default_url);
    String current_version(__firmware_version);

    xout_("current firmware version:");
    xouts(current_version);
    xout_("pull firmware from:");
    xouts(default_url);

    var _ok = stop_schedule();
    Serial.println("start updating, this may takes a few minutes");

    t_httpUpdate_return ret = ESPhttpUpdate.update(espClient, fetch_url, current_version);

    switch (ret)
    {
    case HTTP_UPDATE_FAILED:
        Serial.printf("HTTP_UPDATE_FAILED Error (%d): %s", ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str());
        break;

    case HTTP_UPDATE_NO_UPDATES:
        Serial.println("HTTP_UPDATE_NO_UPDATES");
        break;

    case HTTP_UPDATE_OK:
        Serial.println("HTTP_UPDATE_OK");
        break;
    }
    if (_ok)
        resume_schedule();
    return 0;
}